[TotW] Query Expressions

This Lasso 9 discusses how query expressions provide SQL-like capabilities within Lasso and a powerful alternative to traditional iterates and loops.

Introduction

Query expressions allow data in arrays and other compound data types to be easily iterated, filtered, and manipulated using a natural syntax which is reminiscent of SQL.

In the following code the query expression is written as WITH (temporary variable name) IN (compound data type) DO (codeblock). The temporary variable is set to each element of the compound data type in turn and the codeblock is run.

    local(myArray) = array(1, 2.0, 'three');
    WITH myElement IN #myArray DO log_always(#myElement)

Within this tip the query expression terms will be written in uppercase to distinguish them from other reserved words. However, there is no need to write the terms in uppercase. Lowercase works just as well.

    with myElement in #myArray do log_always(#myElement)

We can see how the query expression resembles a SQL expression that we might pass to MySQL or SQLite except the data source is a local array rather than a database table. Just like in SQL we can add a WHERE clause to our expression in order to filter the results.

The following expression will only log elements from the array which are of type integer. The expression has been reformatted for easier readability.

    WITH myElement IN #myArray
    WHERE #myElement->IsA(::integer)
    DO log_always(#myElement)

There are many other clauses which are possible within the query expression. Each will be discussed below.

Anatomy of a Query Expression

Every query expression consists of three parts.

We will discuss the WITH clause briefly followed by the actions and then the operators that are available.

WITH Clause

The WITH clause always takes the form shown below. The specified variable name is created as a local in the current scope. The compound type is usually an array, but can be any type which has the trait Trait_Queriable including arrays, captures, files, lists, pairs, inlines, SQLite queries, and others.

    WITH (variable name) IN (array) ACTION

Lasso can generate a series of numbers using the following syntax. This code will log the numbers from 1 to 100 to the console. Notice that since this isn't a loop the [Loop_Count] can't be used. Instead, the generated number is available in the local variable specified after the WITH keyword.

    WITH n in 1 to 100 DO { log_always(#n) }

We can add BY to skip through the numbers a fixed amount. This code will log only odd numbers to the console.

    WITH n in 1 to 100 BY 2 DO { log_always(#n) }

With a SELECT action this code can be used to generate a series of numbers for other purposes. This code will log even numbers to the console. The WITH expression is lazy so the numbers for #numberSource are not actually generated until they are used within the following WITH expression.

    local(numberSource) = (WITH n IN 0 TO 100 SELECT #n)

    WITH myNumber in #numberSource
    WHERE #myNumber % 2 == 0
    DO { log_always(#myNumber) };

Actions

The action which is specified in the query expression determines the result of the expression. The DO action is processed immediately. The other actions are "lazy", the result of the action are only returned when they are required.

The following actions are available.

    local(myArray) = array(1, 2, 3, 4, 5, 6, 7);
    WITH myElement IN #myArray
    DO {
        log_always(#myElement)
    }

The following expression returns "1234567" since it uses an auto-collect codeblock.

    local(myArray) = array(1, 2, 3, 4, 5, 6, 7);
    WITH myElement IN #myArray
    DO {^ #myElement ^}

The following expression uses an implicit codeblock. The braces can be omitted if only a single statement is being performed. This expression logs each element to the console and returns no output.

    local(myArray) = array(1, 2, 3, 4, 5, 6, 7);
    WITH myElement IN #myArray
    DO log_always(#myElement)

The following expression simply outputs a string "1, 2, 3, 4, 5, 6, 7". The result of the query expression is a Queriable_Select object which is then automatically cast to string.

    local(myArray) = array(1, 2, 3, 4, 5, 6, 7);
    WITH myElement IN #myArray
    SELECT #myElement

If we want to manipulate the array then we can SELECT a statement rather than the simple variable. The following expression outputs a string "1, 4, 9, 16, 25, 36, 49" with the squares of each of the elements from the array.

    local(myArray) = array(1, 2, 3, 4, 5, 6, 7);
    WITH myElement IN #myArray
    SELECT #myElement * #myElement

We can store this result in a variable by placing the entire query expression in parentheses. The following expression stores the Queriable_Select object in a variable named #mySquares.

    local(myArray) = array(1, 2, 3, 4, 5, 6, 7);
    local(mySquares) = (WITH myElement IN #myArray
            SELECT #myElement * #myElement);
    #mySquares;

We can pass the selected results to another tag for further processing. The following expression uses Iterate tags to cycle through each of the selected values. In this case the squared values will be returned each on a line by themselves.

    local(myArray) = array(1, 2, 3, 4, 5, 6, 7);
    iterate(WITH myElement IN #myArray SELECT myElement * #myElement);
        loop_value; '<br />';
    /iterate;

We can pass the selected results into another query expression. The inner expression in the code below creates a pair from each element. These pairs are used as the data source for the outer query expression which then multiplies the two parts of each pair together.

    local(myArray) = array(1, 2, 3, 4, 5, 6, 7);
    WITH myPair IN
        (WITH myElement IN #myArray
        SELECT pair(#myElement = #myElement))
    SELECT #myPair->First * #myPair->Second;
    local(myArray) = array(1, 2, 3, 4, 5, 6, 7);
    WITH myElement IN #myArray
    SUM #myElement;

SUM can be applied to string results as well. The result of the following expression is the string "SunMonTueWedThuFriSat"

    local(daysOfweek) = array('Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat');
    WITH myElement IN #daysOfweek
    SUM #myElement;
    local(myArray) = array(1, 2, 3, 4, 5, 6, 7);
    WITH myElement IN #myArray
    AVERAGE #myElement;
    local(myArray) = array(1, 2, 3, 4, 5, 6, 7);
    WITH myElement IN #myArray
    MIN #myElement;
    local(myArray) = array(1, 2, 3, 4, 5, 6, 7);
    WITH myElement IN #myArray
    MAX #myElement;

Operators

Much of the power of query expressions come from the operators that are available. Lasso already contains several constructs for cycling through the values in a compound data type. Query expressions allow much of the power of SQL statements to be applied to Lasso's native data types.

Additional operators including GROUP BY and JOIN will be added in a future build.

Above we calculated the SUM of an array of integers from 1 to 7 and found the result was 26. The following expression finds the SUM of just the even elements from the array by checking that each value modulo 2 is equal to 0. The result is 12.

    local(myArray) = array(1, 2, 3, 4, 5, 6, 7);
    WITH myElement IN #myArray
    WHERE #myElement % 2 == 0
    SUM #myElement;

The expression after WHERE can contain additional clauses. The following expressions finds the SUM of even elements greater than 2, returning 6.

    local(myArray) = array(1, 2, 3, 4, 5, 6, 7);
    WITH myElement IN #myArray
    WHERE #myElement % 2 == 0 && #myElement > 4
    SUM #myElement;

It is also valid to have multiple WHERE clauses.

    local(myArray) = array(1, 2, 3, 4, 5, 6, 7);
    WITH myElement IN #myArray
    WHERE #myElement % 2 == 0
    WHERE #myElement > 4
    SUM #myElement;

Rather than specifing a simple expression the WHERE clause can also accept a codeblock. The codeblock must return either True or False.

    local(myArray) = array(1, 2, 3, 4, 5, 6, 7);
    WITH myElement IN #myArray
    WHERE {
        return #myElement % 2 == 0 && #myElement > 4
    }
    SUM #myElement;
    local(myArray) = array(1, 2, 3, 4, 5, 6, 7);
    WITH myElement IN #myArray
    LET mySquare = #myElement * #myElement
    SUM #mySquare;
    local(myArray) = array(1, 2, 3, 4, 5, 6, 7);
    WITH myElement IN #myArray
    SKIP 3
    SUM #myElement;
    local(myArray) = array(1, 2, 3, 4, 5, 6, 7);
    WITH myElement IN #myArray
    SKIP 3
    TAKE 2
    SUM #myElement;

Note that specifying the SKIP and TAKE operators in the opposite order produces very different results since each of the operators is applied in turn. We normally want to SKIP forward through the entire source, then LIMIT to the desired number of elements.

    local(myArray) = array(1, 7, 3, 6, 5, 4, 2);
    WITH myElement IN #myArray
    ORDER BY #myElement
    SELECT #myElement;

An optional DESCENDING keyword reverse the order. This code returns "7, 6, 5, 4, 3, 2, 1".

    local(myArray) = array(1, 7, 3, 6, 5, 4, 2);
    WITH myElement IN #myArray
    ORDER BY #myElement DESCENDING
    SELECT #myElement;